/** @file
  A non-transitional driver for VirtIo 1.0 PCI devices.

  Copyright (C) 2016, Red Hat, Inc.
  Copyright (C) 2017, AMD Inc, All rights reserved.<BR>

  This program and the accompanying materials are licensed and made available
  under the terms and conditions of the BSD License which accompanies this
  distribution. The full text of the license may be found at
  http://opensource.org/licenses/bsd-license.php

  THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS" BASIS, WITHOUT
  WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED.
**/

#include <IndustryStandard/Pci.h>
#include <IndustryStandard/Virtio.h>
#include <Protocol/PciIo.h>
#include <Protocol/PciRootBridgeIo.h>
#include <Protocol/VirtioDevice.h>
#include <Library/BaseMemoryLib.h>
#include <Library/DebugLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/PciCapLib.h>
#include <Library/PciCapPciIoLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiLib.h>

#include "Virtio10.h"


//
// Utility functions
//

/**
  Transfer data between the caller and a register in a virtio-1.0 register
  block.

  @param[in]     PciIo        The EFI_PCI_IO_PROTOCOL instance that represents
                              the device.

  @param[in]     Config       The "fat pointer" structure that identifies the
                              register block to access.

  @param[in]     Write        TRUE if the register should be written, FALSE if
                              the register should be read.

  @param[in]     FieldOffset  The offset of the register within the register
                              block.

  @param[in]     FieldSize    The size of the register within the register
                              block. Can be one of 1, 2, 4 and 8. Accesses to
                              8-byte registers are broken up into two 4-byte
                              accesses.

  @param[in,out] Buffer       When Write is TRUE, the register is written with
                              data from Buffer. When Write is FALSE, the caller
                              receives the register value into Buffer.

  @retval  EFI_SUCCESS            Register access successful.

  @retval  EFI_INVALID_PARAMETER  The register block pointed-to by Config
                                  doesn't exist; or FieldOffset and FieldSize
                                  would overflow the register block; or
                                  FieldSize is invalid.

  @return                         Error codes from
                                  EFI_PCI_IO_PROTOCOL.(Io|Mem).(Read|Write)
                                  member functions.
**/
STATIC
EFI_STATUS
Virtio10Transfer (
  IN     EFI_PCI_IO_PROTOCOL *PciIo,
  IN     VIRTIO_1_0_CONFIG   *Config,
  IN     BOOLEAN             Write,
  IN     UINTN               FieldOffset,
  IN     UINTN               FieldSize,
  IN OUT VOID                *Buffer
  )
{
  UINTN                      Count;
  EFI_PCI_IO_PROTOCOL_WIDTH  Width;
  EFI_PCI_IO_PROTOCOL_ACCESS *BarType;
  EFI_PCI_IO_PROTOCOL_IO_MEM Access;

  if (!Config->Exists ||
      FieldSize > Config->Length ||
      FieldOffset > Config->Length - FieldSize) {
    return EFI_INVALID_PARAMETER;
  }

  Count = 1;
  switch (FieldSize) {
    case 1:
      Width = EfiPciIoWidthUint8;
      break;

    case 2:
      Width = EfiPciIoWidthUint16;
      break;

    case 8:
      Count = 2;
      //
      // fall through
      //

    case 4:
      Width = EfiPciIoWidthUint32;
      break;

    default:
      return EFI_INVALID_PARAMETER;
  }

  BarType = (Config->BarType == Virtio10BarTypeMem) ? &PciIo->Mem : &PciIo->Io;
  Access = Write ? BarType->Write : BarType->Read;

  return Access (PciIo, Width, Config->Bar, Config->Offset + FieldOffset,
           Count, Buffer);
}


/**
  Determine if a PCI BAR is IO or MMIO.

  @param[in]  PciIo     The EFI_PCI_IO_PROTOCOL instance that represents the
                        device.

  @param[in]  BarIndex  The number of the BAR whose type the caller is
                        interested in.

  @param[out] BarType   On output, a VIRTIO_1_0_BAR_TYPE value that gives the
                        type of the BAR.

  @retval EFI_SUCCESS      The BAR type has been recognized and stored in
                           BarType.

  @retval EFI_UNSUPPORTED  The BAR type couldn't be identified.

  @return                  Error codes from
                           EFI_PCI_IO_PROTOCOL.GetBarAttributes().
**/
STATIC
EFI_STATUS
GetBarType (
  IN  EFI_PCI_IO_PROTOCOL *PciIo,
  IN  UINT8               BarIndex,
  OUT VIRTIO_1_0_BAR_TYPE *BarType
  )
{
  EFI_STATUS Status;
  VOID       *Resources;

  Status = PciIo->GetBarAttributes (PciIo, BarIndex, NULL, &Resources);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Status = EFI_UNSUPPORTED;

  if (*(UINT8 *)Resources == ACPI_QWORD_ADDRESS_SPACE_DESCRIPTOR) {
    EFI_ACPI_QWORD_ADDRESS_SPACE_DESCRIPTOR *Descriptor;

    Descriptor = Resources;
    switch (Descriptor->ResType) {
    case ACPI_ADDRESS_SPACE_TYPE_MEM:
      *BarType = Virtio10BarTypeMem;
      Status = EFI_SUCCESS;
      break;

    case ACPI_ADDRESS_SPACE_TYPE_IO:
      *BarType = Virtio10BarTypeIo;
      Status = EFI_SUCCESS;
      break;

    default:
      break;
    }
  }

  FreePool (Resources);
  return Status;
}


/*
  Traverse the PCI capabilities list of a virtio-1.0 device, and capture the
  locations of the interesting virtio-1.0 register blocks.

  @param[in,out] Device         The VIRTIO_1_0_DEV structure that identifies
                                the device. On input, the caller is responsible
                                that the Device->PciIo member be live, and that
                                the CommonConfig, NotifyConfig,
                                NotifyOffsetMultiplier and SpecificConfig
                                members be zeroed. On output,  said members
                                will have been updated from the PCI
                                capabilities found.

  @retval EFI_SUCCESS  Traversal successful.

  @return              Error codes from PciCapPciIoLib, PciCapLib, and the
                       GetBarType() helper function.
*/
STATIC
EFI_STATUS
ParseCapabilities (
  IN OUT VIRTIO_1_0_DEV *Device
  )
{
  EFI_STATUS   Status;
  PCI_CAP_DEV  *PciDevice;
  PCI_CAP_LIST *CapList;
  UINT16       VendorInstance;
  PCI_CAP      *VendorCap;

  Status = PciCapPciIoDeviceInit (Device->PciIo, &PciDevice);
  if (EFI_ERROR (Status)) {
    return Status;
  }
  Status = PciCapListInit (PciDevice, &CapList);
  if (EFI_ERROR (Status)) {
    goto UninitPciDevice;
  }

  for (VendorInstance = 0;
       !EFI_ERROR (PciCapListFindCap (CapList, PciCapNormal,
                     EFI_PCI_CAPABILITY_ID_VENDOR, VendorInstance,
                     &VendorCap));
       VendorInstance++) {
    UINT8             CapLen;
    VIRTIO_PCI_CAP    VirtIoCap;
    VIRTIO_1_0_CONFIG *ParsedConfig;

    //
    // Big enough to accommodate a VIRTIO_PCI_CAP structure?
    //
    Status = PciCapRead (PciDevice, VendorCap,
               OFFSET_OF (EFI_PCI_CAPABILITY_VENDOR_HDR, Length), &CapLen,
               sizeof CapLen);
    if (EFI_ERROR (Status)) {
      goto UninitCapList;
    }
    if (CapLen < sizeof VirtIoCap) {
      //
      // Too small, move to next.
      //
      continue;
    }

    //
    // Read interesting part of capability.
    //
    Status = PciCapRead (PciDevice, VendorCap, 0, &VirtIoCap, sizeof VirtIoCap);
    if (EFI_ERROR (Status)) {
      goto UninitCapList;
    }

    switch (VirtIoCap.ConfigType) {
    case VIRTIO_PCI_CAP_COMMON_CFG:
      ParsedConfig = &Device->CommonConfig;
      break;
    case VIRTIO_PCI_CAP_NOTIFY_CFG:
      ParsedConfig = &Device->NotifyConfig;
      break;
    case VIRTIO_PCI_CAP_DEVICE_CFG:
      ParsedConfig = &Device->SpecificConfig;
      break;
    default:
      //
      // Capability is not interesting.
      //
      continue;
    }

    //
    // Save the location of the register block into ParsedConfig.
    //
    Status = GetBarType (Device->PciIo, VirtIoCap.Bar, &ParsedConfig->BarType);
    if (EFI_ERROR (Status)) {
      goto UninitCapList;
    }
    ParsedConfig->Bar    = VirtIoCap.Bar;
    ParsedConfig->Offset = VirtIoCap.Offset;
    ParsedConfig->Length = VirtIoCap.Length;

    if (VirtIoCap.ConfigType == VIRTIO_PCI_CAP_NOTIFY_CFG) {
      //
      // This capability has an additional field called NotifyOffsetMultiplier;
      // parse it too.
      //
      if (CapLen < sizeof VirtIoCap + sizeof Device->NotifyOffsetMultiplier) {
        //
        // Too small, move to next.
        //
        continue;
      }

      Status = PciCapRead (PciDevice, VendorCap, sizeof VirtIoCap,
                 &Device->NotifyOffsetMultiplier,
                 sizeof Device->NotifyOffsetMultiplier);
      if (EFI_ERROR (Status)) {
        goto UninitCapList;
      }
    }

    //
    // Capability parsed successfully.
    //
    ParsedConfig->Exists = TRUE;
  }

  ASSERT_EFI_ERROR (Status);

UninitCapList:
  PciCapListUninit (CapList);

UninitPciDevice:
  PciCapPciIoDeviceUninit (PciDevice);

  return Status;
}


/**
  Accumulate the BAR type of a virtio-1.0 register block into a UINT64
  attribute map, such that the latter is suitable for enabling IO / MMIO
  decoding with EFI_PCI_IO_PROTOCOL.Attributes().

  @param[in]     Config      The "fat pointer" structure that identifies the
                             register block. It is allowed for the register
                             block not to exist.

  @param[in,out] Attributes  On output, if the register block exists,
                             EFI_PCI_IO_ATTRIBUTE_MEMORY or
                             EFI_PCI_IO_ATTRIBUTE_IO is OR-ed into Attributes,
                             according to the register block's BAR type.
**/
STATIC
VOID
UpdateAttributes (
  IN     VIRTIO_1_0_CONFIG *Config,
  IN OUT UINT64            *Attributes
  )
{
  if (Config->Exists) {
    *Attributes |= (Config->BarType == Virtio10BarTypeMem) ?
                     EFI_PCI_IO_ATTRIBUTE_MEMORY:
                     EFI_PCI_IO_ATTRIBUTE_IO;
  }
}


//
// VIRTIO_DEVICE_PROTOCOL member functions
//

STATIC
EFI_STATUS
EFIAPI
Virtio10GetDeviceFeatures (
  IN VIRTIO_DEVICE_PROTOCOL *This,
  OUT UINT64                *DeviceFeatures
  )
{
  VIRTIO_1_0_DEV *Dev;
  UINT32         Selector;
  UINT32         Features32[2];

  Dev = VIRTIO_1_0_FROM_VIRTIO_DEVICE (This);

  for (Selector = 0; Selector < 2; ++Selector) {
    EFI_STATUS Status;

    //
    // Select the low or high half of the features.
    //
    Status = Virtio10Transfer (Dev->PciIo, &Dev->CommonConfig, TRUE,
               OFFSET_OF (VIRTIO_PCI_COMMON_CFG, DeviceFeatureSelect),
               sizeof Selector, &Selector);
    if (EFI_ERROR (Status)) {
      return Status;
    }

    //
    // Fetch that half.
    //
    Status = Virtio10Transfer (Dev->PciIo, &Dev->CommonConfig, FALSE,
               OFFSET_OF (VIRTIO_PCI_COMMON_CFG, DeviceFeature),
               sizeof Features32[Selector], &Features32[Selector]);
    if (EFI_ERROR (Status)) {
      return Status;
    }
  }

  *DeviceFeatures = LShiftU64 (Features32[1], 32) | Features32[0];
  return EFI_SUCCESS;
}


STATIC
EFI_STATUS
EFIAPI
Virtio10SetGuestFeatures (
  IN VIRTIO_DEVICE_PROTOCOL  *This,
  IN UINT64                   Features
  )
{
  VIRTIO_1_0_DEV *Dev;
  UINT32         Selector;
  UINT32         Features32[2];

  Dev = VIRTIO_1_0_FROM_VIRTIO_DEVICE (This);

  Features32[0] = (UINT32)Features;
  Features32[1] = (UINT32)RShiftU64 (Features, 32);

  for (Selector = 0; Selector < 2; ++Selector) {
    EFI_STATUS Status;

    //
    // Select the low or high half of the features.
    //
    Status = Virtio10Transfer (Dev->PciIo, &Dev->CommonConfig, TRUE,
               OFFSET_OF (VIRTIO_PCI_COMMON_CFG, DriverFeatureSelect),
               sizeof Selector, &Selector);
    if (EFI_ERROR (Status)) {
      return Status;
    }

    //
    // Write that half.
    //
    Status = Virtio10Transfer (Dev->PciIo, &Dev->CommonConfig, TRUE,
               OFFSET_OF (VIRTIO_PCI_COMMON_CFG, DriverFeature),
               sizeof Features32[Selector], &Features32[Selector]);
    if (EFI_ERROR (Status)) {
      return Status;
    }
  }

  return EFI_SUCCESS;
}


STATIC
EFI_STATUS
EFIAPI
Virtio10SetQueueAddress (
  IN VIRTIO_DEVICE_PROTOCOL  *This,
  IN VRING                   *Ring,
  IN UINT64                  RingBaseShift
  )
{
  VIRTIO_1_0_DEV *Dev;
  EFI_STATUS     Status;
  UINT64         Address;
  UINT16         Enable;

  Dev = VIRTIO_1_0_FROM_VIRTIO_DEVICE (This);

  Address = (UINTN)Ring->Desc;
  Address += RingBaseShift;
  Status = Virtio10Transfer (Dev->PciIo, &Dev->CommonConfig, TRUE,
             OFFSET_OF (VIRTIO_PCI_COMMON_CFG, QueueDesc),
             sizeof Address, &Address);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Address = (UINTN)Ring->Avail.Flags;
  Address += RingBaseShift;
  Status = Virtio10Transfer (Dev->PciIo, &Dev->CommonConfig, TRUE,
             OFFSET_OF (VIRTIO_PCI_COMMON_CFG, QueueAvail),
             sizeof Address, &Address);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Address = (UINTN)Ring->Used.Flags;
  Address += RingBaseShift;
  Status = Virtio10Transfer (Dev->PciIo, &Dev->CommonConfig, TRUE,
             OFFSET_OF (VIRTIO_PCI_COMMON_CFG, QueueUsed),
             sizeof Address, &Address);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Enable = 1;
  Status = Virtio10Transfer (Dev->PciIo, &Dev->CommonConfig, TRUE,
             OFFSET_OF (VIRTIO_PCI_COMMON_CFG, QueueEnable),
             sizeof Enable, &Enable);
  return Status;
}


STATIC
EFI_STATUS
EFIAPI
Virtio10SetQueueSel (
  IN VIRTIO_DEVICE_PROTOCOL  *This,
  IN UINT16                   Index
  )
{
  VIRTIO_1_0_DEV *Dev;
  EFI_STATUS     Status;

  Dev = VIRTIO_1_0_FROM_VIRTIO_DEVICE (This);

  Status = Virtio10Transfer (Dev->PciIo, &Dev->CommonConfig, TRUE,
             OFFSET_OF (VIRTIO_PCI_COMMON_CFG, QueueSelect),
             sizeof Index, &Index);
  return Status;
}


STATIC
EFI_STATUS
EFIAPI
Virtio10SetQueueNotify (
  IN VIRTIO_DEVICE_PROTOCOL  *This,
  IN UINT16                   Index
  )
{
  VIRTIO_1_0_DEV *Dev;
  EFI_STATUS     Status;
  UINT16         SavedQueueSelect;
  UINT16         NotifyOffset;

  Dev = VIRTIO_1_0_FROM_VIRTIO_DEVICE (This);

  //
  // Read NotifyOffset first. NotifyOffset is queue specific, so we have
  // to stash & restore the current queue selector around it.
  //
  // So, start with saving the current queue selector.
  //
  Status = Virtio10Transfer (Dev->PciIo, &Dev->CommonConfig, FALSE,
             OFFSET_OF (VIRTIO_PCI_COMMON_CFG, QueueSelect),
             sizeof SavedQueueSelect, &SavedQueueSelect);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  //
  // Select the requested queue.
  //
  Status = Virtio10Transfer (Dev->PciIo, &Dev->CommonConfig, TRUE,
             OFFSET_OF (VIRTIO_PCI_COMMON_CFG, QueueSelect),
             sizeof Index, &Index);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  //
  // Read the QueueNotifyOff field.
  //
  Status = Virtio10Transfer (Dev->PciIo, &Dev->CommonConfig, FALSE,
             OFFSET_OF (VIRTIO_PCI_COMMON_CFG, QueueNotifyOff),
             sizeof NotifyOffset, &NotifyOffset);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  //
  // Re-select the original queue.
  //
  Status = Virtio10Transfer (Dev->PciIo, &Dev->CommonConfig, TRUE,
             OFFSET_OF (VIRTIO_PCI_COMMON_CFG, QueueSelect),
             sizeof SavedQueueSelect, &SavedQueueSelect);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  //
  // We can now kick the queue.
  //
  Status = Virtio10Transfer (Dev->PciIo, &Dev->NotifyConfig, TRUE,
             NotifyOffset * Dev->NotifyOffsetMultiplier,
             sizeof Index, &Index);
  return Status;
}


STATIC
EFI_STATUS
EFIAPI
Virtio10SetQueueAlign (
  IN VIRTIO_DEVICE_PROTOCOL  *This,
  IN UINT32                   Alignment
  )
{
  return (Alignment == EFI_PAGE_SIZE) ? EFI_SUCCESS : EFI_UNSUPPORTED;
}


STATIC
EFI_STATUS
EFIAPI
Virtio10SetPageSize (
  IN VIRTIO_DEVICE_PROTOCOL  *This,
  IN UINT32                   PageSize
  )
{
  return (PageSize == EFI_PAGE_SIZE) ? EFI_SUCCESS : EFI_UNSUPPORTED;
}


STATIC
EFI_STATUS
EFIAPI
Virtio10GetQueueNumMax (
  IN  VIRTIO_DEVICE_PROTOCOL  *This,
  OUT UINT16                  *QueueNumMax
  )
{
  VIRTIO_1_0_DEV *Dev;
  EFI_STATUS     Status;

  Dev = VIRTIO_1_0_FROM_VIRTIO_DEVICE (This);

  Status = Virtio10Transfer (Dev->PciIo, &Dev->CommonConfig, FALSE,
             OFFSET_OF (VIRTIO_PCI_COMMON_CFG, QueueSize),
             sizeof *QueueNumMax, QueueNumMax);
  return Status;
}


STATIC
EFI_STATUS
EFIAPI
Virtio10SetQueueNum (
  IN VIRTIO_DEVICE_PROTOCOL  *This,
  IN UINT16                   QueueSize
  )
{
  EFI_STATUS     Status;
  UINT16         CurrentSize;

  //
  // This member function is required for VirtIo MMIO, and a no-op in
  // VirtIo PCI 0.9.5. In VirtIo 1.0, drivers can theoretically use this
  // member to reduce memory consumption, but none of our drivers do. So
  // just check that they set the size that is already in effect.
  //
  Status = Virtio10GetQueueNumMax (This, &CurrentSize);
  if (EFI_ERROR (Status)) {
    return Status;
  }
  return (CurrentSize == QueueSize) ? EFI_SUCCESS : EFI_UNSUPPORTED;
}


STATIC
EFI_STATUS
EFIAPI
Virtio10GetDeviceStatus (
  IN  VIRTIO_DEVICE_PROTOCOL  *This,
  OUT UINT8                   *DeviceStatus
  )
{
  VIRTIO_1_0_DEV *Dev;
  EFI_STATUS     Status;

  Dev = VIRTIO_1_0_FROM_VIRTIO_DEVICE (This);

  Status = Virtio10Transfer (Dev->PciIo, &Dev->CommonConfig, FALSE,
             OFFSET_OF (VIRTIO_PCI_COMMON_CFG, DeviceStatus),
             sizeof *DeviceStatus, DeviceStatus);
  return Status;
}


STATIC
EFI_STATUS
EFIAPI
Virtio10SetDeviceStatus (
  IN VIRTIO_DEVICE_PROTOCOL  *This,
  IN UINT8                   DeviceStatus
  )
{
  VIRTIO_1_0_DEV *Dev;
  EFI_STATUS     Status;

  Dev = VIRTIO_1_0_FROM_VIRTIO_DEVICE (This);

  Status = Virtio10Transfer (Dev->PciIo, &Dev->CommonConfig, TRUE,
             OFFSET_OF (VIRTIO_PCI_COMMON_CFG, DeviceStatus),
             sizeof DeviceStatus, &DeviceStatus);
  return Status;
}


STATIC
EFI_STATUS
EFIAPI
Virtio10WriteDevice (
  IN VIRTIO_DEVICE_PROTOCOL *This,
  IN UINTN                  FieldOffset,
  IN UINTN                  FieldSize,
  IN UINT64                 Value
  )
{
  VIRTIO_1_0_DEV *Dev;
  EFI_STATUS     Status;

  Dev = VIRTIO_1_0_FROM_VIRTIO_DEVICE (This);

  Status = Virtio10Transfer (Dev->PciIo, &Dev->SpecificConfig, TRUE,
             FieldOffset, FieldSize, &Value);
  return Status;
}


STATIC
EFI_STATUS
EFIAPI
Virtio10ReadDevice (
  IN  VIRTIO_DEVICE_PROTOCOL *This,
  IN  UINTN                  FieldOffset,
  IN  UINTN                  FieldSize,
  IN  UINTN                  BufferSize,
  OUT VOID                   *Buffer
  )
{
  VIRTIO_1_0_DEV *Dev;
  EFI_STATUS     Status;

  if (FieldSize != BufferSize) {
    return EFI_INVALID_PARAMETER;
  }

  Dev = VIRTIO_1_0_FROM_VIRTIO_DEVICE (This);

  Status = Virtio10Transfer (Dev->PciIo, &Dev->SpecificConfig, FALSE,
             FieldOffset, FieldSize, Buffer);
  return Status;
}

STATIC
EFI_STATUS
EFIAPI
Virtio10AllocateSharedPages (
  IN     VIRTIO_DEVICE_PROTOCOL  *This,
  IN     UINTN                   Pages,
  IN OUT VOID                    **HostAddress
  )
{
  VIRTIO_1_0_DEV *Dev;
  EFI_STATUS     Status;

  Dev = VIRTIO_1_0_FROM_VIRTIO_DEVICE (This);

  Status = Dev->PciIo->AllocateBuffer (
                         Dev->PciIo,
                         AllocateAnyPages,
                         EfiBootServicesData,
                         Pages,
                         HostAddress,
                         EFI_PCI_ATTRIBUTE_MEMORY_CACHED
                         );
  return Status;
}

STATIC
VOID
EFIAPI
Virtio10FreeSharedPages (
  IN  VIRTIO_DEVICE_PROTOCOL  *This,
  IN  UINTN                   Pages,
  IN  VOID                    *HostAddress
  )
{
  VIRTIO_1_0_DEV *Dev;

  Dev = VIRTIO_1_0_FROM_VIRTIO_DEVICE (This);

  Dev->PciIo->FreeBuffer (
                Dev->PciIo,
                Pages,
                HostAddress
                );
}

STATIC
EFI_STATUS
EFIAPI
Virtio10MapSharedBuffer (
  IN     VIRTIO_DEVICE_PROTOCOL  *This,
  IN     VIRTIO_MAP_OPERATION    Operation,
  IN     VOID                    *HostAddress,
  IN OUT UINTN                   *NumberOfBytes,
  OUT    EFI_PHYSICAL_ADDRESS    *DeviceAddress,
  OUT    VOID                    **Mapping
  )
{
  EFI_STATUS                    Status;
  VIRTIO_1_0_DEV                *Dev;
  EFI_PCI_IO_PROTOCOL_OPERATION PciIoOperation;

  Dev = VIRTIO_1_0_FROM_VIRTIO_DEVICE (This);

  //
  // Map VIRTIO_MAP_OPERATION to EFI_PCI_IO_PROTOCOL_OPERATION
  //
  switch (Operation) {
  case VirtioOperationBusMasterRead:
    PciIoOperation = EfiPciIoOperationBusMasterRead;
    break;
  case VirtioOperationBusMasterWrite:
    PciIoOperation = EfiPciIoOperationBusMasterWrite;
    break;
  case VirtioOperationBusMasterCommonBuffer:
    PciIoOperation = EfiPciIoOperationBusMasterCommonBuffer;
    break;
  default:
    return EFI_INVALID_PARAMETER;
  }

  Status = Dev->PciIo->Map (
                         Dev->PciIo,
                         PciIoOperation,
                         HostAddress,
                         NumberOfBytes,
                         DeviceAddress,
                         Mapping
                         );
  return Status;
}

STATIC
EFI_STATUS
EFIAPI
Virtio10UnmapSharedBuffer (
  IN  VIRTIO_DEVICE_PROTOCOL  *This,
  IN  VOID                    *Mapping
  )
{
  EFI_STATUS      Status;
  VIRTIO_1_0_DEV  *Dev;

  Dev = VIRTIO_1_0_FROM_VIRTIO_DEVICE (This);

  Status = Dev->PciIo->Unmap (
                         Dev->PciIo,
                         Mapping
                         );

  return Status;
}

STATIC CONST VIRTIO_DEVICE_PROTOCOL mVirtIoTemplate = {
  VIRTIO_SPEC_REVISION (1, 0, 0),
  0,                              // SubSystemDeviceId, filled in dynamically
  Virtio10GetDeviceFeatures,
  Virtio10SetGuestFeatures,
  Virtio10SetQueueAddress,
  Virtio10SetQueueSel,
  Virtio10SetQueueNotify,
  Virtio10SetQueueAlign,
  Virtio10SetPageSize,
  Virtio10GetQueueNumMax,
  Virtio10SetQueueNum,
  Virtio10GetDeviceStatus,
  Virtio10SetDeviceStatus,
  Virtio10WriteDevice,
  Virtio10ReadDevice,
  Virtio10AllocateSharedPages,
  Virtio10FreeSharedPages,
  Virtio10MapSharedBuffer,
  Virtio10UnmapSharedBuffer
};


//
// EFI_DRIVER_BINDING_PROTOCOL member functions
//

STATIC
EFI_STATUS
EFIAPI
Virtio10BindingSupported (
  IN EFI_DRIVER_BINDING_PROTOCOL *This,
  IN EFI_HANDLE                  DeviceHandle,
  IN EFI_DEVICE_PATH_PROTOCOL    *RemainingDevicePath
  )
{
  EFI_STATUS          Status;
  EFI_PCI_IO_PROTOCOL *PciIo;
  PCI_TYPE00          Pci;

  Status = gBS->OpenProtocol (DeviceHandle, &gEfiPciIoProtocolGuid,
                  (VOID **)&PciIo, This->DriverBindingHandle,
                  DeviceHandle, EFI_OPEN_PROTOCOL_BY_DRIVER);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Status = PciIo->Pci.Read (PciIo, EfiPciIoWidthUint32, 0,
                        sizeof Pci / sizeof (UINT32), &Pci);
  if (EFI_ERROR (Status)) {
    goto CloseProtocol;
  }

  Status = EFI_UNSUPPORTED;
  //
  // Recognize non-transitional modern devices. Also, we'll have to parse the
  // PCI capability list, so make sure the CapabilityPtr field will be valid.
  //
  if (Pci.Hdr.VendorId == VIRTIO_VENDOR_ID &&
      Pci.Hdr.DeviceId >= 0x1040 &&
      Pci.Hdr.DeviceId <= 0x107F &&
      Pci.Hdr.RevisionID >= 0x01 &&
      Pci.Device.SubsystemID >= 0x40 &&
      (Pci.Hdr.Status & EFI_PCI_STATUS_CAPABILITY) != 0) {
    //
    // The virtio-vga device is special. It can be driven both as a VGA device
    // with a linear framebuffer, and through its underlying, modern,
    // virtio-gpu-pci device, which has no linear framebuffer itself. For
    // compatibility with guest OSes that insist on inheriting a linear
    // framebuffer from the firmware, we should leave virtio-vga to
    // QemuVideoDxe, and support only virtio-gpu-pci here.
    //
    // Both virtio-vga and virtio-gpu-pci have DeviceId 0x1050, but only the
    // former has device class PCI_CLASS_DISPLAY_VGA.
    //
    if (Pci.Hdr.DeviceId != 0x1050 || !IS_PCI_VGA (&Pci)) {
      Status = EFI_SUCCESS;
    }
  }

CloseProtocol:
  gBS->CloseProtocol (DeviceHandle, &gEfiPciIoProtocolGuid,
         This->DriverBindingHandle, DeviceHandle);

  return Status;
}


STATIC
EFI_STATUS
EFIAPI
Virtio10BindingStart (
  IN EFI_DRIVER_BINDING_PROTOCOL *This,
  IN EFI_HANDLE                  DeviceHandle,
  IN EFI_DEVICE_PATH_PROTOCOL    *RemainingDevicePath
  )
{
  VIRTIO_1_0_DEV *Device;
  EFI_STATUS     Status;
  PCI_TYPE00     Pci;
  UINT64         SetAttributes;

  Device = AllocateZeroPool (sizeof *Device);
  if (Device == NULL) {
    return EFI_OUT_OF_RESOURCES;
  }

  Device->Signature = VIRTIO_1_0_SIGNATURE;
  CopyMem (&Device->VirtIo, &mVirtIoTemplate, sizeof mVirtIoTemplate);

  Status = gBS->OpenProtocol (DeviceHandle, &gEfiPciIoProtocolGuid,
                  (VOID **)&Device->PciIo, This->DriverBindingHandle,
                  DeviceHandle, EFI_OPEN_PROTOCOL_BY_DRIVER);
  if (EFI_ERROR (Status)) {
    goto FreeDevice;
  }

  Status = Device->PciIo->Pci.Read (Device->PciIo, EfiPciIoWidthUint32, 0,
                                sizeof Pci / sizeof (UINT32), &Pci);
  if (EFI_ERROR (Status)) {
    goto ClosePciIo;
  }

  Device->VirtIo.SubSystemDeviceId = Pci.Hdr.DeviceId - 0x1040;

  Status = ParseCapabilities (Device);
  if (EFI_ERROR (Status)) {
    goto ClosePciIo;
  }

  Status = Device->PciIo->Attributes (Device->PciIo,
                            EfiPciIoAttributeOperationGet, 0,
                            &Device->OriginalPciAttributes);
  if (EFI_ERROR (Status)) {
    goto ClosePciIo;
  }

  SetAttributes = (EFI_PCI_IO_ATTRIBUTE_BUS_MASTER |
                   EFI_PCI_IO_ATTRIBUTE_DUAL_ADDRESS_CYCLE);
  UpdateAttributes (&Device->CommonConfig, &SetAttributes);
  UpdateAttributes (&Device->NotifyConfig, &SetAttributes);
  UpdateAttributes (&Device->SpecificConfig, &SetAttributes);
  Status = Device->PciIo->Attributes (Device->PciIo,
                            EfiPciIoAttributeOperationEnable, SetAttributes,
                            NULL);
  if (EFI_ERROR (Status)) {
    goto ClosePciIo;
  }

  Status = gBS->InstallProtocolInterface (&DeviceHandle,
                  &gVirtioDeviceProtocolGuid, EFI_NATIVE_INTERFACE,
                  &Device->VirtIo);
  if (EFI_ERROR (Status)) {
    goto RestorePciAttributes;
  }

  return EFI_SUCCESS;

RestorePciAttributes:
  Device->PciIo->Attributes (Device->PciIo, EfiPciIoAttributeOperationSet,
                   Device->OriginalPciAttributes, NULL);

ClosePciIo:
  gBS->CloseProtocol (DeviceHandle, &gEfiPciIoProtocolGuid,
         This->DriverBindingHandle, DeviceHandle);

FreeDevice:
  FreePool (Device);

  return Status;
}


STATIC
EFI_STATUS
EFIAPI
Virtio10BindingStop (
  IN EFI_DRIVER_BINDING_PROTOCOL *This,
  IN EFI_HANDLE                  DeviceHandle,
  IN UINTN                       NumberOfChildren,
  IN EFI_HANDLE                  *ChildHandleBuffer
  )
{
  EFI_STATUS             Status;
  VIRTIO_DEVICE_PROTOCOL *VirtIo;
  VIRTIO_1_0_DEV         *Device;

  Status = gBS->OpenProtocol (DeviceHandle, &gVirtioDeviceProtocolGuid,
                  (VOID **)&VirtIo, This->DriverBindingHandle,
                  DeviceHandle, EFI_OPEN_PROTOCOL_GET_PROTOCOL);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Device = VIRTIO_1_0_FROM_VIRTIO_DEVICE (VirtIo);

  Status = gBS->UninstallProtocolInterface (DeviceHandle,
                  &gVirtioDeviceProtocolGuid, &Device->VirtIo);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Device->PciIo->Attributes (Device->PciIo, EfiPciIoAttributeOperationSet,
                   Device->OriginalPciAttributes, NULL);
  gBS->CloseProtocol (DeviceHandle, &gEfiPciIoProtocolGuid,
         This->DriverBindingHandle, DeviceHandle);
  FreePool (Device);

  return EFI_SUCCESS;
}


STATIC EFI_DRIVER_BINDING_PROTOCOL mDriverBinding = {
  &Virtio10BindingSupported,
  &Virtio10BindingStart,
  &Virtio10BindingStop,
  0x10, // Version
  NULL, // ImageHandle, to be overwritten
  NULL  // DriverBindingHandle, to be overwritten
};


//
// EFI_COMPONENT_NAME_PROTOCOL and EFI_COMPONENT_NAME2_PROTOCOL
// implementations
//

STATIC
EFI_UNICODE_STRING_TABLE mDriverNameTable[] = {
  { "eng;en", L"Virtio 1.0 PCI Driver" },
  { NULL,     NULL                     }
};

STATIC
EFI_COMPONENT_NAME_PROTOCOL mComponentName;

STATIC
EFI_STATUS
EFIAPI
Virtio10GetDriverName (
  IN  EFI_COMPONENT_NAME_PROTOCOL *This,
  IN  CHAR8                       *Language,
  OUT CHAR16                      **DriverName
  )
{
  return LookupUnicodeString2 (
           Language,
           This->SupportedLanguages,
           mDriverNameTable,
           DriverName,
           (BOOLEAN)(This == &mComponentName) // Iso639Language
           );
}

STATIC
EFI_STATUS
EFIAPI
Virtio10GetDeviceName (
  IN  EFI_COMPONENT_NAME_PROTOCOL *This,
  IN  EFI_HANDLE                  DeviceHandle,
  IN  EFI_HANDLE                  ChildHandle,
  IN  CHAR8                       *Language,
  OUT CHAR16                      **ControllerName
  )
{
  return EFI_UNSUPPORTED;
}

STATIC
EFI_COMPONENT_NAME_PROTOCOL mComponentName = {
  &Virtio10GetDriverName,
  &Virtio10GetDeviceName,
  "eng"
};

STATIC
EFI_COMPONENT_NAME2_PROTOCOL mComponentName2 = {
  (EFI_COMPONENT_NAME2_GET_DRIVER_NAME)     &Virtio10GetDriverName,
  (EFI_COMPONENT_NAME2_GET_CONTROLLER_NAME) &Virtio10GetDeviceName,
  "en"
};


//
// Entry point of this driver
//

EFI_STATUS
EFIAPI
Virtio10EntryPoint (
  IN EFI_HANDLE       ImageHandle,
  IN EFI_SYSTEM_TABLE *SystemTable
  )
{
  return EfiLibInstallDriverBindingComponentName2 (
           ImageHandle,
           SystemTable,
           &mDriverBinding,
           ImageHandle,
           &mComponentName,
           &mComponentName2
           );
}
